package org.nebuloop.photo.organizer;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.nebuloop.photo.organizer.dao.ImageDAO;
import org.nebuloop.photo.organizer.data.ImageInfo;
import org.nebuloop.photo.organizer.data.ImageSource;
import org.nebuloop.photo.organizer.util.Config;
import org.nebuloop.photo.organizer.util.DateUtil;
import org.nebuloop.photo.organizer.util.FileUtil;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;

public class ImageProcessor {

  private static final Logger LOGGER = Logger.getLogger(ImageProcessor.class.getCanonicalName());
  private ImageDAO imageDao;
  private String destinationDirectory;
  private String skippedFilesDestinationDirectory;
  private FileUtil fileUtil;
  private String sourceId;
  private SkippedFileLogger skippedFileLogger;
  private DestDirCreator destDirCreator;

  public ImageProcessor() {
    fileUtil = new FileUtil();
    destDirCreator = DestDirCreator.getInstance();
  }

  public void processFile(File imageFile) throws IOException {
    try {
      boolean rs = processImageFile(imageFile);
      if (!rs) {
        skippedFileLogger.logFileSkipEvent(imageFile, "File already present", null);
      }
    } catch (BadMetadataException ex) {
      LOGGER.log(Level.WARNING, "Metadata error in processing file " + imageFile.getAbsolutePath()
          + ". Copying into skipped directory", ex);
      processSkippedFile(imageFile);
    } catch (Exception ex) {
      skippedFileLogger.logFileSkipEvent(imageFile, null, ex);
      LOGGER
          .log(Level.WARNING, "Error in processing file " + imageFile.getAbsolutePath() + ". Skipping this file.", ex);
    }
  }

  /**
   * 
   * @param imageFile
   * @return true if file was copied, false if file was not copied (such as it
   *         already exist)
   * @throws IOException
   * @throws ImageProcessingException
   * @throws MetadataException
   * @throws SQLException
   */
  public boolean processImageFile(File imageFile) throws IOException, SQLException, BadMetadataException {
    final String imageAbsolutePath = imageFile.getCanonicalPath();
    LOGGER.log(Level.FINE, "Processing file : {0}", imageAbsolutePath);
    if (!imageFile.exists()) {
      throw new IOException("Image file " + imageAbsolutePath + " does not exist");
    }
    if (!imageFile.isFile()) {
      throw new IOException("Image file " + imageAbsolutePath + " is not a file");
    }
    String md5sum = fileUtil.md5sum(imageFile);
    ImageInfo imageInfo = imageDao.getImageByMD5(md5sum);

    if (imageInfo != null) {
      File dstFile = new File(imageInfo.getDestPath());
      if (dstFile.exists()) {
        LOGGER.log(Level.INFO, "Image {0} already exists in the destination...skipping", imageAbsolutePath);
        ImageSource imageSource = new ImageSource();
        imageSource.setImgId(imageInfo.getId());
        imageSource.setSourcePath(imageAbsolutePath);
        imageSource.setSource_id(sourceId);
        imageDao.saveImageSourceInfo(imageSource);

        return false;
      } else {
        LOGGER.log(Level.WARNING, "Image {0} info already exists but the file is missing. Continueing with copy...",
            imageAbsolutePath);
        // continue with copying
      }
    }
    LOGGER.log(Level.INFO, "Image {0} does not exist in the destination yet", imageAbsolutePath);

    imageInfo = new ImageInfo();
    imageInfo.setMd5(md5sum);
    imageInfo.setName(imageFile.getName());
    try {

      Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
      if (metadata == null) {
        throw new BadMetadataException("No Metadata found for media " + imageAbsolutePath);
      }
      ExifSubIFDDirectory exifSubIFDDir = metadata.getDirectory(ExifSubIFDDirectory.class);
      ExifIFD0Directory exifIFD0Dir = metadata.getDirectory(ExifIFD0Directory.class);
      GpsDirectory gpsDir = metadata.getDirectory(GpsDirectory.class);
      if (exifSubIFDDir == null) {
        StringBuilder sb = new StringBuilder();
        for(Directory directory : metadata.getDirectories()) {
          sb.append(directory.getClass().getName()).append(",");
        }
        LOGGER.warning("Found only metadata (" + sb.toString() + ") for image " + imageAbsolutePath);
        throw new BadMetadataException("No Date related Data found for image " + imageAbsolutePath);
      }

      Date imageCreateTime = extractImageDate(exifSubIFDDir, exifIFD0Dir);
      imageInfo.setImageCreationTime(imageCreateTime);

      imageInfo.setHasExif(true);
      boolean hasGeocodes =  (gpsDir != null && gpsDir.getGeoLocation() != null);
      imageInfo.setHasGeocodes(hasGeocodes);
      LOGGER.info("Image " + imageAbsolutePath + " has GeoTags : " + hasGeocodes);
      
      
      String strDestDir = destDirCreator.getImageOutputPath(imageFile, destinationDirectory, metadata, imageCreateTime);
      
      File destDir = new File(strDestDir);
      File destFile = copyImage(imageFile, destDir);
      imageInfo.setDestPath(destFile.getAbsolutePath());

      applyImageEnrichment(metadata, destFile);
      imageDao.saveImageInfo(imageInfo);
      ImageInfo savedImageInfo = imageDao.getImageByMD5(md5sum);
      ImageSource imageSource = new ImageSource();
      imageSource.setImgId(savedImageInfo.getId());
      imageSource.setSourcePath(imageAbsolutePath);
      imageSource.setSource_id(sourceId);
      imageDao.saveImageSourceInfo(imageSource);
    } catch (com.drew.lang.CompoundException e) {
      throw new BadMetadataException("Bad metadata", e);
    }

    return true;
  }

  public static Date extractImageDate(ExifSubIFDDirectory exifSubIFDDir, ExifIFD0Directory exifIFD0Dir) throws BadMetadataException, IOException {
    boolean tagDateTimeOriginalPresent = exifSubIFDDir.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
    boolean tagDateTimeDigitizedPresent = exifSubIFDDir.containsTag(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED);
    boolean tagDateTimeOtherPresent = exifIFD0Dir.containsTag(ExifIFD0Directory.TAG_DATETIME);
    
    if (! (tagDateTimeOriginalPresent || tagDateTimeDigitizedPresent || tagDateTimeOtherPresent) ) {
      throw new BadMetadataException("Image Metadata does not have Create Date anywhere");
      // LOGGER.log(Level.WARNING,
      // "Image {0} Does not have Create Date...skipping",
      // imageAbsolutePath);
      // return false;
    }
    Date imageCreateTime;
    if (tagDateTimeOriginalPresent)
      imageCreateTime = exifSubIFDDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
    else if (tagDateTimeDigitizedPresent)
      imageCreateTime = exifSubIFDDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED);
    else
      imageCreateTime = exifIFD0Dir.getDate(ExifIFD0Directory.TAG_DATETIME);
    
    LOGGER.log(Level.FINE, "Image Create Date : {0,date,yyyy-MM-dd HH:mm:ss}", imageCreateTime);
    Calendar cal = Calendar.getInstance();
    cal.setTime(imageCreateTime);
    int imageCreationYear = cal.get(Calendar.YEAR);
    // note Date object getYear() returns a value by subtracting 1900
    if (imageCreationYear < 1990 || imageCreationYear > Config.CURRENT_YEAR) {
      throw new IOException("Image Creation Year : " + imageCreationYear + " is invalid. Not processing file");
    }
    return imageCreateTime;
  }

  protected void processSkippedFile(File imageFile) throws IOException {
    String destPathStr;
    File srcPath = imageFile.getParentFile();

    String srcDirLeaf = srcPath.getName();

    Calendar dirDate = DateUtil.parseDateAsCalendar(srcDirLeaf);

    if (dirDate != null) {
      
      destPathStr = destDirCreator.getImageOutputPath(null, dirDate.getTime(), imageFile,
          skippedFilesDestinationDirectory, null, null);
    } else {
      if (srcPath.isAbsolute()) {
        Path sp = srcPath.toPath();
        Path np = sp.subpath(1, sp.getNameCount());
        srcPath = np.toFile();
      }

      destPathStr = (srcPath == null) ? "unsorted" : srcPath.getPath();
    }

    File destPath = new File(skippedFilesDestinationDirectory, destPathStr);
    File destFile = copyImage(imageFile, destPath);

    LOGGER.log(Level.FINE, "Copying {0} ==> {1}",
        new Object[] { imageFile.getCanonicalPath(), destFile.getCanonicalPath() });
  }

  /**
   * Copies the given file into the specified location. If a file with the same
   * name already exist at the destination location, it will append a numeric
   * prefix to create unique file name
   * 
   * @param srcFile
   * @param destDir
   * @return
   * @throws IOException
   */
  public File copyImage(File srcFile, File destDir) throws IOException {

    FileUtil.mkdirs(destDir);

    File destFile = new File(destDir, srcFile.getName());

    // If the destination file already exist, append some sequence number to
    // the destination file
    int suffix = 1;
    while (destFile.exists()) {
      if (suffix > 100) {
        throw new IOException("Tried to find new name 100 times, still did not for file " + destFile);
      }
      String fname = srcFile.getName();
      int li = fname.lastIndexOf('.');
      StringBuilder newName = new StringBuilder(fname.substring(0, li));
      newName.append('_').append(suffix).append(fname.substring(li));
      suffix++;
      destFile = new File(destDir, newName.toString());
    }

    if (!destFile.createNewFile()) {
      throw new IOException("Destination File " + destFile.getAbsolutePath() + " already exist..not copying "
          + srcFile.getAbsolutePath());
    }

    FileUtil.copyFile(srcFile, destFile);
    return destFile;
  }

  public void applyImageEnrichment(Metadata metadata, File destFile) {
    // do nothing for now
  }

  public ImageDAO getImageDao() {
    return imageDao;
  }

  public void setImageDao(ImageDAO imageDao) {
    this.imageDao = Objects.requireNonNull(imageDao);
  }

  public String getDestinationDirectory() {
    return destinationDirectory;
  }

  public void setDestinationDirectory(String destinationDirectory) {
    this.destinationDirectory = Objects.requireNonNull(destinationDirectory);
  }

  public String getSourceId() {
    return sourceId;
  }

  public void setSourceId(String sourceId) {
    this.sourceId = sourceId;
  }

  public boolean isPreserveNonNumericFolderName() {
    return DestDirCreator.getInstance().isPreserveNonNumericFolderName();
  }

  public void setPreserveNonNumericFolderName(boolean preserveNonNumericFolderName) {
    DestDirCreator.getInstance().setPreserveNonNumericFolderName(preserveNonNumericFolderName);
  }

  public String getSkippedFilesDestinationDirectory() {
    return skippedFilesDestinationDirectory;
  }

  public void setSkippedFilesDestinationDirectory(String skippedFilesDestinationDirectory) {
    this.skippedFilesDestinationDirectory = skippedFilesDestinationDirectory;
  }

  public SkippedFileLogger getSkippedFileLogger() {
    return skippedFileLogger;
  }

  public void setSkippedFileLogger(SkippedFileLogger skippedFileLogger) {
    this.skippedFileLogger = skippedFileLogger;
  }

  public boolean isUseCameraInfolderName() {
    return DestDirCreator.getInstance().isUseCameraInFolderName();
  }

  public void setUseCameraInfolderName(boolean useCameraInfolderName) {
    DestDirCreator.getInstance().setUseCameraInFolderName(useCameraInfolderName);
  }
}
